package com.google.bitcoin.db;

import com.google.bitcoin.core.*;
import java.util.List;
import java.util.LinkedList;
import java.util.Collection;
import java.util.Set;
import java.util.HashSet;


import java.io.IOException;
import java.io.File;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;



import java.math.BigInteger;


import org.slf4j.Logger;
import org.slf4j.LoggerFactory;



public class WalletDB extends KeyStore implements WalletInterface
{
    public static final long serialVersionUID = 3L;
    private static final Logger log = LoggerFactory.getLogger(WalletDB.class);

    private transient TransactionDB db;
    private HashSet<Sha256Hash> pending_tx_list;

    private HashSet<Address> relevant_addresses;


    public WalletDB(NetworkParameters params)
    {
        super(params);
        pending_tx_list=new HashSet<Sha256Hash>();
        relevant_addresses=new HashSet<Address>();
        
    }
    public void setDb(TransactionDB db)
    {
        this.db=db;
        
    }
    public void addRelevantAddresses(Iterable<Address> addr)
    {

        for(Address a : addr)
        {
            relevant_addresses.add(a);
        }
    }


    public boolean isTransactionRelevant(Transaction tx, boolean includeDoubleSpending) throws ScriptException
    {
        try
        {
            for(TransactionOutput out : tx.getOutputs())
            {
                Address addr = out.getScriptPubKey().getToAddress();
                if (isAddressRelevant(addr))
                {
                    log.info("Relevant");
                    return true;
                }
            }
            for(TransactionInput in : tx.getInputs())
            {
                if (!in.isCoinBase())
                {
                    Address addr = in.getFromAddress();
                    if (isAddressRelevant(addr))
                    {
                        log.info("Relevant " + tx.getHash());
                        return true;
                    }
                }
            }
        }
        catch(com.google.bitcoin.core.ScriptException e)
        {
            log.info("Script exception, probably not relevant", e);
        }

        return false;

    }

    public boolean isAddressRelevant(Address addr)
    {
        return (relevant_addresses.contains(addr));
    }


    public void reorganize(List<StoredBlock> oldBlocks, List<StoredBlock> newBlocks) throws VerificationException{}


    public void receiveFromBlock(Transaction tx, StoredBlock block, BlockChain.NewBlockType blockType) 
        throws VerificationException, ScriptException
    {
        db.receiveFromBlock(tx, block, blockType);
    }

    public Transaction getTransaction(Sha256Hash hash)
    {
        return db.getTransaction(hash);
    }
    public Transaction getTransaction(Sha256Hash hash, int connected_out_levels)
    {
        return db.getTransaction(hash, connected_out_levels);
    }

    public PeerEventListener getPeerEventListener()
    {
        return new AbstractPeerEventListener() {
                @Override
                public void onTransaction(Peer peer, Transaction t) {
                    // Runs locked on a peer thread.
                    try {
                        
                        receivePending(t);
                    } catch (VerificationException e) {
                        log.warn("Received broadcast transaction that does not validate: {}", t);
                        log.warn("VerificationException caught", e);
                    } catch (ScriptException e) {
                        log.warn("Received broadcast transaction with not understood scripts: {}", t);
                        log.warn("ScriptException caught", e);
                    }
                }
            }
        ;
    }

    public Collection<Transaction> getPendingTransactions()
    {
        LinkedList<Transaction> lst= new LinkedList<Transaction>();
        LinkedList<Sha256Hash> to_remove=new LinkedList<Sha256Hash>();
        HashSet<Sha256Hash> copy = new HashSet<Sha256Hash>();

        synchronized(pending_tx_list)
        {
            copy.addAll(pending_tx_list);
        }
            for(Sha256Hash hash : copy)
            {
                Transaction tx = getTransaction(hash);
                if ((tx==null) || (tx.getConfidence().getConfidenceType()==TransactionConfidence.ConfidenceType.BUILDING))
                {
                    to_remove.add(hash);
                }
                else
                {
                    lst.add(tx);
                }
            }
        synchronized(pending_tx_list)
        {
            for(Sha256Hash hash : to_remove)
            {
                pending_tx_list.remove(hash);
            }
        }
        return lst;
    }
    public Collection<TransactionOutput> getUnspentOutputs()
    {
        return db.getUnspentOutputs(getAddresses());

    }
    public Collection<TransactionOutput> getUnspentOutputs(Address addr)
    {
        return db.getUnspentOutputs(addr);
    }
    public Collection<TransactionOutput> getUnspentOutputs(Address addr, BigInteger min_value)
    {
        return db.getUnspentOutputs(addr, min_value);
    }
 
    public Collection<TransactionOutput> getUnspentOutputs(Collection<Address> addresses)
    {
        return db.getUnspentOutputs(addresses);
    }
    public Set<Transaction> getTransactions(boolean includeDead, boolean includeInactive)
    {
        return db.getTransactions(includeDead, includeInactive);

    }

    public void commitTx(Transaction tx) throws VerificationException, DoubleSpendException
    {
        synchronized(pending_tx_list)
        {
            pending_tx_list.add(tx.getHash());
        }
        db.saveTx(tx, false);
    }

    public void receivePending(Transaction tx) throws VerificationException, ScriptException
    {
        if (isTransactionRelevant(tx, true))
        {
            try
            {
                db.saveTx(tx, true);
            }
            catch(DoubleSpendException e){throw new RuntimeException(e);}
        }
    }

    public BigInteger getBalance()
    {
        return getBalance(BalanceType.AVAILABLE);
    }
    public BigInteger getBalance(BalanceType balanceType)
    {
        HashSet<Address> set = new HashSet<Address>();
        set.addAll(getAddresses());
        log.info("Address count: " + set.size());
        return db.getBalance(balanceType, set);
    }
    public BigInteger getBalance(BalanceType balanceType, Address addr)
    {
        return db.getBalance(balanceType, addr);
    }
    public BigInteger getBalance(BalanceType balanceType, HashSet<Address> addrs)
    {
        return db.getBalance(balanceType, addrs);
    }


    public synchronized void saveToFileStream(OutputStream out) throws IOException
    {
        ObjectOutputStream oos=new ObjectOutputStream(out);
        oos.writeObject(this);
        oos.flush();
        oos.close();
    }

    public void saveToFile(File f) throws IOException
    {
        File dir = f.getParentFile();

        if (dir==null) dir=new File(".");


        File tmp = File.createTempFile("bitcoin-",".wallet", dir);

        saveToFileStream(new FileOutputStream(tmp));

        tmp.renameTo(f);
    }

    public static WalletDB loadFromFile(File f) throws IOException
    {
        return loadFromFileStream(new FileInputStream(f));
    }

    public static WalletDB loadFromFileStream(InputStream in) throws IOException
    {
        ObjectInputStream ois = new ObjectInputStream(in);

        try{
            WalletDB wallet = (WalletDB)ois.readObject();
            ois.close();
            return wallet;
        }
        catch(Exception e){throw new IOException(e);}


    }

    @Override
    public long getEarliestKeyCreationTime()
    {
        return super.getEarliestKeyCreationTime();
    }

    @Override
    public void addKey(ECKey key)
    {
        super.addKey(key);
        relevant_addresses.addAll(getAddresses());

    }

    public void clearCache()
    {
        db.clearCache();
    }


}


